local _M = { _VERSION = "0.1" }

local bit       = require "bit"
local aes       = require "resty.aes"
local msgpack   = require "resty.luajit-msgpack-pure"
local cjson     = require "cjson.safe"
      cjson.encode_max_depth(32)
      cjson.decode_max_depth(32)
local random    = require "resty.random"
local str       = require "resty.string"
local base64url = require "shared.base64url"

-- const
local NULL  = ngx.null

local DEF = {
  GAME_TOKEN = 1,
}

local FIELDS = {
  -- game session
  [DEF.GAME_TOKEN] = {
    "server",
    "session",
    "game",
    "provider",
    "player",
    "wallet",
    "currency",
    "dealer",
    "dealerPlayer",
    "casino",
    "account",
    "accountType",
    "ip",
    "issueAt",
    "ttl",
    "permissions",
    "extraData",
  },
}


local Datagram = {}
do
  local DATAGRAM = Datagram

  function DATAGRAM.pack( ticket, version )
    if type(ticket) ~= "table" then
      return nil, "INVALID_MODEL"
    end
    if tonumber(version) == nil then
      return nil, "INVALID_DATAGRAM_VERSION"
    end

    local fields = FIELDS[version]
    if not fields then
      return nil, "INVALID_DATAGRAM_VERSION"
    end

    local data = { version }
    for i, k in ipairs(fields) do
      local v = ticket[k]
      if v == nil  or  v == cjson.null  then
        data[#data + 1] = NULL
      else
        data[#data + 1] = v
      end
    end
    -- return cjson.encode(data)
    return msgpack.pack(data)
  end


  function DATAGRAM.unpack( buffer )
    -- local datagram = cjson.decode(buffer)
    local _, datagram = msgpack.unpack(buffer)

    if type(datagram) ~= "table" then
      return nil, "INVALID_DATAGRAM"
    end

    local version = datagram[1]
    if type(version) ~= "number" then
      return nil, "INVALID_DATAGRAM_VERSION"
    end

    local fields = FIELDS[version]
    if not fields then
      return nil, "INVALID_DATAGRAM_VERSION"
    end

    local reply = {}
    for i, k in ipairs(fields) do
      local v = datagram[i + 1]
      if v ~= NULL then
        reply[k] = v
      else
        reply[k] = nil
      end
    end
    return reply, version
  end
end


local Cipher = {}
do
  local CIPHER = Cipher
  local CIPHER_mt = { __index = CIPHER }

  function CIPHER.new( conf )
    return setmetatable({
        KEYs = conf.KEYs,
        IVs  = conf.IVs
      },
      CIPHER_mt)
  end

  function CIPHER.get_cipher( self, seed )
    if not seed then
      seed = 0
    end
    local iv  = self.IVs [ bit.band(seed, 0x07) + 1 ]
    local key = self.KEYs[ bit.band(bit.rshift(seed, 3), 0x07) + 1]

    return assert(aes:new(key, nil, aes.cipher(128, "cbc"),
                          {iv=iv}))
  end
end


-- _M
do
  local _Chiper = Cipher.new({
    KEYs = {
      "60d365d8f7474aca",  "5eb8aa8edbf63f28",
      "c3d5622444672bc1",  "da2c6a9482e03447",
      "c5c1ac57c1dc8dd2",  "fe4e93d3acdefa4c",
      "89e7478bd7e59432",  "7cdf7f2100d5f04d",  },
    IVs = {
      "f29928bde0d43fba",  "72b255e6816199ff",
      "d07dd912e32a855f",  "05c7f75e101939af",
      "b7e71d82c535fe58",  "b8e62097c717b1a6",
      "1ae115de4da93fe9",  "91e0cb62286477be",  }
  })

  --[[  pack the auth-ticket.
    argument:
      ticket
      version
    return:
      token   (string)
      (error)
    remark:
      the token formation:   to_base64url( <prefix> )   "."   to_base64url( <body> )
  --]]
  local function pack( ticket, version )
    local data, err = Datagram.pack(ticket, version)
    if err then
      return nil, err
    end

    local prefix = random.bytes(3)
    local seed   = tonumber(str.to_hex(string.sub(prefix, 3, 3), 16))
    local cipher = _Chiper:get_cipher(seed)
    return base64url.encode(prefix) .. "." .. base64url.encode( cipher:encrypt(data) )
  end


  --[[ unpack the auth-ticket.
    argument:
      token     (string base64url)
    return:
      ticket    (table)
      version   (number)
      (error)
  ]]
  local function unpack( token )
    if token == nil then
      return nil, nil
    end

    local prefix = base64url.decode( string.sub(token, 1, 4) )
    if not prefix then
      return nil, nil, "INVALID_TOKEN"
    end

    local code   = string.sub(token, 6)
    local seed   = tonumber(str.to_hex(string.sub(prefix, 3, 3), 16))
    local cipher = _Chiper:get_cipher(seed)
    local crypted = base64url.decode(code)
    if crypted then
      local content = cipher:decrypt( crypted )
      if content then
        return Datagram.unpack(content)
      end
    end
    return nil, nil
  end


  --[[ is expired.
    argument:
      ticket      (any)
      timestamp  (timestamp)
    return:
      ticket    (table)
      version   (number)
      (error)
  ]]
  local function is_expired( ticket, timestamp )
    assert(type(timestamp) == "number", "invalid argument 'timestamp'")

    if type(ticket)=="table"  and  tonumber(ticket.issueAt)  and  tonumber(ticket.ttl)  then
      local issueAt = tonumber(ticket.issueAt)
      local ttl     = tonumber(ticket.ttl)
      if ttl > 0 then
        return (issueAt + ttl) < timestamp
      end
    end
    return false
  end


  local function validate( ticket, version )
    assert(type(ticket)=="table", "invalid argument 'ticket'")
    assert(version              , "invalid argument 'version'")

    local fields = FIELDS[version]
    if fields then
      for _, k in ipairs(fields) do
        local v = ticket[k]
        if v == nil then
          if k ~="permissions"  and  k ~="extraData"  then
            return false, string.format("missing field '%s'", k)
          end
        else
          if k =="ttl"  or  k =="issueAt" then
            if not tonumber(v) then
              return false, string.format("invalid cast to number from field '%s'", k)
            end
          elseif k =="permissions"  or  k =="extraData"  then
            if "table" ~= type(v) then
              return false, string.format("field '%s' is not of type table", k)
            end
          end
        end
      end
    end
    return true
  end


  -- exports
  _M.def        = DEF
  _M.pack       = pack
  _M.unpack     = unpack
  _M.is_expired = is_expired
  _M.validate   = validate
end


-- Ticket
do
  local TICKET = {}
  local TICKET_mt = {__index = TICKET}

  function TICKET.new(ticket)
    assert(ticket ~= nil, "invalid argument 'ticket'")

    return setmetatable(ticket, TICKET_mt)
  end


  function TICKET.has_permission(self, permission)
    assert(self ~= nil, "invalid argument 'self'")

    if self.permissions  and  "table"==type(self.permissions)  and  next(self.permissions) then
      for i = 1, #self.permissions do
        if permission == self.permissions[i] then
          return true
        end
      end
    end
    return false
  end

  -- export
  _M.ticket = TICKET
end

return _M
